import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import javax.swing.*;

public class Heart extends JPanel {
    CubicCurve2D.Double[] curves = new CubicCurve2D.Double[2];
    Point2D.Double[] points;
    boolean showPath = false;

    public Heart() {
        int[][] cds = {
            { 192, 139 }, { 201,  35 }, { 348,  47 }, { 312, 163 },
            { 312, 163 }, { 291, 235 }, { 237, 301 }, { 192, 324 }
        };
        points = new Point2D.Double[cds.length];
        for(int j = 0; j < cds.length; j++) {
            points[j] = new Point2D.Double(cds[j][0], cds[j][1]);
        }
        curves[0] = new CubicCurve2D.Double();
        curves[0].setCurve(points, 0);
        curves[1] = new CubicCurve2D.Double();
        curves[1].setCurve(points, 4);
        setBackground(Color.white);
        new HeartPointMover(this);
    }

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
        int w = getWidth();
        // Get left half of heart.
        Path2D.Double leftSide = getReflection(w);
        if(!showPath) {
            // Show construction and allow user to drag/print points.
            // Draw vertical construction line at mid-width.
            g2.setPaint(new Color(200,230,210));
            g2.drawLine(w/2, 0, w/2, getHeight());
            // Draw curves.
            g2.setPaint(Color.blue);
            for(int i = 0; i < curves.length; i++) {
                g2.draw(curves[i]);
            }
            // Connect control points across curve juncture
            // to help position them for smooth connection.
            g2.setPaint(Color.pink);
            g2.draw(new Line2D.Double(points[2], points[5]));
            // Mark curve end and control points.
            for(int j = 0; j < points.length; j++) {
                mark(g2, j);
            }
            // Draw left half of heart.
            g2.setPaint(Color.pink);
            g2.draw(leftSide);
        } else {
            // Show a path with no construction.
            Path2D.Double path = new Path2D.Double();
            path.append(curves[0], false);
            path.append(curves[1], false);
            path.append(leftSide, false);
            g2.setPaint(Color.blue);
            g2.draw(path);
        }
    }

    private Path2D.Double getReflection(int w) {
        Path2D.Double path = new Path2D.Double();
        AffineTransform at = AffineTransform.getTranslateInstance(w,0);
        at.scale(-1,1);
        Path2D.Double p = new Path2D.Double(curves[0]);
        path.append(p.getPathIterator(at), false);
        p = new Path2D.Double(curves[1]);
        path.append(p.getPathIterator(at), false);
        return path;
    }

    public void toggleShowPath() {
        showPath = !showPath;
        repaint();
    }

    public void setPoint(Point2D.Double p, double x, double y) {
        p.setLocation(x, y);
        if(p.equals(points[3])) {
            points[4].setLocation(x, y);
        }
        setCurves();
    }

    private void setCurves() {
        for(int i = 0; i < curves.length; i++) {
            curves[i].setCurve(points, i*4);
        }
        repaint();
    }

    private void mark(Graphics2D g2, int index) {
        double x = points[index].x;
        double y = points[index].y;
        int i = (index+1) % 4;
        Color color = (i == 2 || i == 3) ? Color.green.darker() : Color.red;
        g2.setPaint(color);
        g2.fill(new Ellipse2D.Double(x-2, y-2, 4, 4));
    }

    public static void main(String[] args) {
        JFrame f = new JFrame("Heart");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(new Heart());
        f.setSize(400,400);
        f.setLocation(200,200);
        f.setVisible(true);
    }
}

class HeartPointMover extends MouseAdapter {
    Heart component;
    Point2D.Double selectedPoint;
    Cursor cursor;
    Cursor defaultCursor = Cursor.getDefaultCursor();
    Point2D.Double offset = new Point2D.Double();
    boolean dragging = false;
    final int PROX_DIST = 5;

    public HeartPointMover(Heart h) {
        component = h;
        BufferedImage image = getImage();
        Point hotspot = new Point(17,17);
        cursor = Toolkit.getDefaultToolkit()
                        .createCustomCursor(image, hotspot, null);
        component.addMouseListener(this);
        component.addMouseMotionListener(this);
    }

    public void mousePressed(MouseEvent e) {
        if(SwingUtilities.isRightMouseButton(e)) {         // show path
            component.toggleShowPath();
        } else if(!component.showPath) {                   // or construction
            if(selectedPoint != null) {                    // drag points
                offset.x = e.getX() - selectedPoint.x;
                offset.y = e.getY() - selectedPoint.y;
                dragging = true;
            } else {                                       // write points
                printCurvePoints();
                System.out.println("--------------------------");
            }
        }
    }

    private void printCurvePoints() {
        Point2D.Double[] pts = component.points;
        for(int j = 0; j < pts.length; j++) {
            System.out.printf("points[%d] = [%5.1f, %5.1f]%n",
                               j, pts[j].x, pts[j].y);
        }
    }

    public void mouseReleased(MouseEvent e) {
        dragging = false;
    }

    public void mouseDragged(MouseEvent e) {
        if(dragging) {
            double x = e.getX() - offset.x;
            double y = e.getY() - offset.y;
            component.setPoint(selectedPoint, x, y);
        }
    }

    /** For point selection. */
    public void mouseMoved(MouseEvent e) {
        Point p = e.getPoint();
        Point2D.Double[] pts = component.points;
        boolean hovering = false;
        for(int j = 0; j < pts.length; j++) {
            if(j == 4) continue;
            if(pts[j].distance(p) < PROX_DIST) {
                hovering = true;
                if(selectedPoint != pts[j]) {
                    selectedPoint = pts[j];
                    component.setCursor(cursor);
                    break;
                }
            }
        }

        if(!hovering && selectedPoint != null) {
            selectedPoint = null;
            component.setCursor(defaultCursor);
        }
    }

    private BufferedImage getImage() {
        int w = 27, h = 27,
            type = BufferedImage.TYPE_INT_ARGB_PRE;
        BufferedImage image = new BufferedImage(w, h, type);
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setPaint(new Color(0x333333));
        g2.draw(new Line2D.Double(w/2, 0, w/2, 8));    // n
        g2.draw(new Line2D.Double(0, h/2, 8, h/2));    // w
        g2.draw(new Line2D.Double(w/2, h-8, w/2, h));  // s
        g2.draw(new Line2D.Double(w-8, h/2, w, h/2));  // e
        g2.dispose();
        return image;
    }
}